feat: add Retry pattern#240
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Test Results 1 files 1 suites 1m 36s ⏱️ Results for commit e5b4105. ♻️ This comment has been updated with latest results. |
🔍 PR Validation ResultsVersion: `` ✅ Validation Steps
📊 ArtifactsDry-run artifacts have been uploaded and will be available for 7 days. This comment was automatically generated by the PR validation workflow. |
There was a problem hiding this comment.
Pull request overview
Adds a first-class Retry cloud resilience pattern to PatternKit, including a runtime RetryPolicy<TResult> API (sync/async), a Roslyn source generator for attributed policy factories, and an importable inventory lookup demo with docs and coverage.
Changes:
- Introduces
PatternKit.Cloud.Retry.RetryPolicy<TResult>+RetryResult<TResult>with configurable attempts, result/exception predicates, and backoff/delay support. - Adds
[GenerateRetryPolicy]generator + retry predicate attributes, diagnostics, and corresponding generator tests/coverage. - Adds inventory retry example (fluent + generated + DI) and updates pattern/example catalogs + documentation TOCs.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| test/PatternKit.Tests/Cloud/Retry/RetryPolicyTests.cs | Runtime retry policy unit coverage (sync/async, config validation). |
| test/PatternKit.Generators.Tests/RetryPolicyGeneratorTests.cs | Verifies generated retry factory output + generator diagnostics. |
| test/PatternKit.Generators.Tests/AbstractionsAttributeCoverageTests.cs | Ensures new retry attributes are covered by attribute coverage suite. |
| test/PatternKit.Examples.Tests/RetryDemo/InventoryRetryDemoTests.cs | Validates fluent vs generated retry behavior and DI registration in the demo. |
| test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitPatternCatalogTests.cs | Ensures Retry is represented in the production-readiness pattern catalog expectations. |
| test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs | Ensures the new example is resolvable/usable through the “import all examples” DI surface. |
| src/PatternKit.Core/Cloud/Retry/RetryPolicy.cs | Implements the runtime retry policy + builder + async execution API. |
| src/PatternKit.Generators.Abstractions/Retry/RetryAttributes.cs | Adds generator-facing attributes for retry policy generation. |
| src/PatternKit.Generators/Retry/RetryPolicyGenerator.cs | Implements the incremental generator + diagnostics for retry policies. |
| src/PatternKit.Generators/AnalyzerReleases.Unshipped.md | Registers new PKRET00x diagnostics for release tracking. |
| src/PatternKit.Examples/RetryDemo/InventoryRetryDemo.cs | Adds the inventory retry demo (fluent + generated + DI composition). |
| src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs | Wires the new retry demo into the “PatternKit examples” DI integration. |
| src/PatternKit.Examples/ProductionReadiness/PatternKitPatternCatalog.cs | Adds Retry pattern entry to the catalog (docs/source/tests/examples paths). |
| src/PatternKit.Examples/ProductionReadiness/PatternKitExampleCatalog.cs | Adds the inventory retry example descriptor to the example catalog. |
| docs/patterns/toc.yml | Adds Retry under a new Cloud Architecture section in the patterns TOC. |
| docs/patterns/cloud/retry.md | New Retry pattern documentation page. |
| docs/guides/pattern-coverage.md | Updates coverage matrix to include Retry + generator coverage. |
| docs/generators/toc.yml | Adds Retry generator docs to generator TOC. |
| docs/generators/retry.md | New Retry generator documentation page. |
| docs/generators/index.md | Lists Retry under generator inventory and quick reference. |
| docs/examples/toc.yml | Adds Inventory Retry Policy to examples TOC. |
| docs/examples/inventory-retry-policy.md | New example documentation for the inventory retry policy demo. |
Comments suppressed due to low confidence (2)
src/PatternKit.Core/Cloud/Retry/RetryPolicy.cs:156
- Same as the sync path: when a retryable exception is caught,
lastValueis left as whatever the last successful result was, so failures can return a staleValuealongside the last handled exception. Consider clearinglastValue(or tracking last outcome) inside the retryable-exception catch.
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) when (_shouldRetryException(ex))
{
lastException = ex;
}
src/PatternKit.Examples/RetryDemo/InventoryRetryDemo.cs:82
- The generated policy configuration sets
InitialDelayMilliseconds = 0withBackoffFactor = 2, which results in zero delay for all retries with the current backoff calculation. Consider using a non-zero initial delay if the example is meant to demonstrate exponential backoff, or setBackoffFactor = 1/omit it if immediate retry is intended.
[GenerateRetryPolicy(
typeof(InventoryResponse),
FactoryMethodName = "CreateGeneratedPolicy",
PolicyName = "inventory-availability",
MaxAttempts = 3,
InitialDelayMilliseconds = 0,
BackoffFactor = 2)]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static Builder Create(string name = "retry") => new(name); | ||
|
|
| private TimeSpan _initialDelay = TimeSpan.Zero; | ||
| private double _backoffFactor = 1; | ||
| private ResultPredicate _shouldRetryResult = static _ => false; | ||
| private ExceptionPredicate _shouldRetryException = static _ => true; |
| return RetryResult<TResult>.Success(value, attempt); | ||
| } | ||
| catch (Exception ex) when (_shouldRetryException(ex)) | ||
| { |
| sb.Append("abstract "); | ||
| else if (type.IsSealed && type.TypeKind == TypeKind.Class) | ||
| sb.Append("sealed "); | ||
| sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine(); |
| private static void Generate(SourceProductionContext context, INamedTypeSymbol type, TypeDeclarationSyntax node, AttributeData attribute) | ||
| { | ||
| if (!node.Modifiers.Any(static modifier => modifier.Text == "partial")) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(MustBePartial, node.Identifier.GetLocation(), type.Name)); | ||
| return; | ||
| } | ||
|
|
||
| var resultType = attribute.ConstructorArguments.Length >= 1 | ||
| ? attribute.ConstructorArguments[0].Value as INamedTypeSymbol | ||
| : null; | ||
| if (resultType is null) | ||
| return; | ||
|
|
| public static RetryPolicy<InventoryResponse> CreateFluentPolicy() | ||
| => RetryPolicy<InventoryResponse> | ||
| .Create("inventory-availability") | ||
| .WithMaxAttempts(3) | ||
| .WithInitialDelay(TimeSpan.Zero) | ||
| .WithExponentialBackoff(2) | ||
| .HandleResult(static response => response.StatusCode == 408 || response.StatusCode == 429 || response.StatusCode >= 500) | ||
| .HandleException(static exception => exception is TimeoutException) | ||
| .Build(); |
| - Keep `MaxAttempts` bounded and pair retries with cancellation. | ||
| - Retry only idempotent work, or work protected by an idempotency key. | ||
| - Use result predicates for service status codes and exception predicates for transient exceptions. | ||
| - Prefer zero or injected delay providers in tests; production callers can use real delays and exponential backoff. |
| public Builder WithExponentialBackoff(double factor) | ||
| { | ||
| if (factor < 1) | ||
| throw new ArgumentOutOfRangeException(nameof(factor), factor, "Backoff factor must be at least 1."); | ||
|
|
||
| _backoffFactor = factor; | ||
| return this; | ||
| } |
| var maxAttempts = GetNamedInt(attribute, "MaxAttempts") ?? 3; | ||
| var initialDelayMilliseconds = GetNamedInt(attribute, "InitialDelayMilliseconds") ?? 0; | ||
| var backoffFactor = GetNamedDouble(attribute, "BackoffFactor") ?? 1d; | ||
| if (maxAttempts < 1 || initialDelayMilliseconds < 0 || backoffFactor < 1) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(InvalidConfiguration, node.Identifier.GetLocation(), type.Name)); | ||
| return; | ||
| } |
dda80ba to
b341aa0
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #240 +/- ##
==========================================
+ Coverage 91.45% 96.48% +5.02%
==========================================
Files 276 280 +4
Lines 26277 26622 +345
Branches 3651 3707 +56
==========================================
+ Hits 24032 25686 +1654
+ Misses 981 936 -45
+ Partials 1264 0 -1264
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
b341aa0 to
6116014
Compare
| private TimeSpan _initialDelay = TimeSpan.Zero; | ||
| private double _backoffFactor = 1; | ||
| private ResultPredicate _shouldRetryResult = static _ => false; | ||
| private ExceptionPredicate _shouldRetryException = static _ => true; |
| sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); | ||
| if (type.IsStatic) | ||
| sb.Append("static "); | ||
| else if (type.IsAbstract && type.TypeKind == TypeKind.Class) | ||
| sb.Append("abstract "); | ||
| else if (type.IsSealed && type.TypeKind == TypeKind.Class) | ||
| sb.Append("sealed "); | ||
| sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine(); | ||
| sb.AppendLine("{"); |
6116014 to
e5b4105
Compare
Code Coverage |
Summary
Closes #227
Local validation
Note: full local test/example builds still hit the existing local compiler/analyzer mismatch (CS9057: local compiler 5.0 cannot load analyzer built against 5.3), so CI is the authoritative full validation.